/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package com.elcom.emocbc.cbcgateway.httpclient;

import com.elcom.emocbc.cbcgateway.httpclient.xml.BroadcastAck;
import com.elcom.emocbc.cbgw.message.BindRequest;
import com.elcom.emocbc.cbgw.message.BroadcastSubmitRequest;
import com.elcom.emocbc.cbgw.message.Request;
import com.elcom.emocbc.cbgw.message.Unbind;
import com.elcom.emocbc.cbgw.message.metadata.DefaultValueLoader;
import com.elcom.emocbc.cbgw.message.metadata.XMLConstant;
import com.elcom.emocbc.cbgw.message.metadata.XmlApiException;
import com.elcom.util.Constant;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;
import org.apache.http.HttpEntity;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.jconfig.Configuration;
import org.jconfig.ConfigurationManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 *
 * @author GiangLT
 */
public class CbcHttpClient {

    private HttpClient httpClient;
    private ResponseHandler<BroadcastAck> responseHandler;
    private HttpPost httpPost;
    private boolean isBound;
    private DefaultValueLoader defaultValueLoader;
    private static Logger logger = LoggerFactory.getLogger(CbcHttpClient.class);
    private static CbcHttpClient instance = new CbcHttpClient();
    private final int MAX_XML_BIND_RETRY = 3;
    private int xmlBindRetry = 0;
    public final static String FAILURE_INVALID_USER = "InvalidUser";

    public static synchronized CbcHttpClient getInstance() {
        if (instance == null) {
            instance = new CbcHttpClient();
        }
        return instance;
    }

    private CbcHttpClient() {
        connect();

        Configuration config = ConfigurationManager.getConfiguration(Constant.SYS_CONFIG_NAME);
        String defaultValueLoaderFile = config.getProperty("defaultValueLoaderFile", "", "general");
        defaultValueLoader = new DefaultValueLoader(defaultValueLoaderFile);
    }

    public final synchronized void connect() {
        Configuration config = ConfigurationManager.getConfiguration(Constant.SYS_CONFIG_NAME);
        //Threadsafe
        //fix the Error
        //<Invalid use of SingleClientConnManager: connection still allocated>
        String uriStr = config.getProperty("cbcUri", "http://10.149.110.243:5454", "httpConnection");
        logger.warn("Initiating HTTP connection to {}", uriStr);
        httpClient = new DefaultHttpClient();
        ClientConnectionManager mgr = httpClient.getConnectionManager();
        HttpParams params = httpClient.getParams();
        HttpConnectionParams.setConnectionTimeout(params, config.getIntProperty("connectionTimeout", 5000, "httpConnection"));
        HttpConnectionParams.setSoTimeout(params, config.getIntProperty("soTimeout", 5000, "httpConnection"));
        httpClient = new DefaultHttpClient(new ThreadSafeClientConnManager(mgr.getSchemeRegistry()), params);

        responseHandler = new AckResponseHandler();
        isBound = false;

        try {
            URI uri = new URI(uriStr);
            httpPost = new HttpPost(uri);
            logger.warn("Initiated HTTP connection to {}", uriStr);
        } catch (URISyntaxException ex) {
            logger.error("URI syntax error: {}", uriStr, ex);
        }
    }

    private synchronized BroadcastAck sendUncheck(Request request) throws UnsupportedEncodingException, ClientProtocolException, IOException {
        String xmlString = request.getXmlString();
        HttpEntity httpEntity = new StringEntity(xmlString);
        httpPost.setEntity(httpEntity);

        logger.info("==> Sending HttpRequest to [{}]: {}", httpPost.getURI().toString(), request.getXmlString());
        return httpClient.execute(httpPost, responseHandler);
    }

    public synchronized BroadcastAck send(Request request) throws UnsupportedEncodingException, ClientProtocolException, XmlApiException, IOException {
        if (request instanceof Unbind) {
            return unbind();
        }

        if (request instanceof BindRequest) {
            return bind((BindRequest) request);
        }

        if (!isBound) {
            forceUnbind();
            bind();
        }

        if (request instanceof BroadcastSubmitRequest) {
            BroadcastSubmitRequest broadcastSubmitRequest = (BroadcastSubmitRequest) request;
            loadBroadcastParams(broadcastSubmitRequest);
            request = setTimeForBroadcastSubmitRequest(broadcastSubmitRequest);
        }

        defaultValueLoader.loadDefaultValues(request);
        BroadcastAck broadcastAck = sendUncheck(request);
        String status = broadcastAck.getAckMessageType().getStatus().getVal();
        if (status.equalsIgnoreCase(XMLConstant.Completed)) {
            xmlBindRetry = 0;
            return broadcastAck;
        } else {
            String failureReason = broadcastAck.getAckMessageType().getFailureReason();
//            if (failureReason.equals(FAILURE_INVALID_USER) && xmlBindRetry < MAX_XML_BIND_RETRY) {
//                logger.error("Invalid user - retrying time(s): {}", xmlBindRetry++);
//                isBound = false;
//                shutdownConnection();
//                connect();
//                return send(request);//false
//            }
            throw new XmlApiException(failureReason, failureReason, status);
        }
    }

    public synchronized BroadcastAck bind(String cbeRequestId, String username, String password) throws UnsupportedEncodingException, ClientProtocolException, XmlApiException, IOException {
        BindRequest bindRequest = new BindRequest();
        bindRequest.setCBERequestID(cbeRequestId);
        bindRequest.setUserName(username);
        bindRequest.setPassword(password);

        return bind(bindRequest);
    }

    public synchronized BroadcastAck bind() throws UnsupportedEncodingException, ClientProtocolException, XmlApiException, IOException {
        BindRequest bindRequest = new BindRequest();
        defaultValueLoader.loadDefaultValues(bindRequest);

        return bind(bindRequest);
    }

    public synchronized BroadcastAck bind(BindRequest bindRequest) throws UnsupportedEncodingException, ClientProtocolException, XmlApiException, IOException {
        BroadcastAck bindRequestAck = sendUncheck(bindRequest);
        String status = bindRequestAck.getAckMessageType().getStatus().getVal();
        if (status.equals(XMLConstant.Completed)) {
            isBound = true;
            logger.info("Bind successfully");
            return bindRequestAck;
        } else {
            String failureReason = bindRequestAck.getAckMessageType().getFailureReason();
            if (failureReason.equalsIgnoreCase(XMLConstant.CBE_ALRDYBIND)) {
                isBound = true;
                logger.warn("CBE is already bound");
                return bindRequestAck;
            } else {
                isBound = false;
                logger.error("Cannot bind to CBC server: {}", failureReason);
                throw new XmlApiException(failureReason, failureReason, bindRequestAck.getAckMessageType().getStatus().getVal());
            }
        }
    }

    public synchronized BroadcastAck unbind() throws UnsupportedEncodingException, ClientProtocolException, XmlApiException, IOException {
        Unbind unbind = new Unbind();
        defaultValueLoader.loadDefaultValues(unbind);
        BroadcastAck broadcastAck = sendUncheck(unbind);
        String status = broadcastAck.getAckMessageType().getStatus().getVal();

        if (status.equalsIgnoreCase(XMLConstant.Completed)) {
            isBound = false;
            return broadcastAck;
        } else {
            isBound = true;
            String failureReason = broadcastAck.getAckMessageType().getFailureReason();
            throw new XmlApiException(failureReason, failureReason, unbind().getAckMessageType().getStatus().getVal());
        }

    }

    public synchronized BroadcastAck forceUnbind() throws UnsupportedEncodingException, ClientProtocolException, IOException {
        logger.debug("Forcing unbind");
        Unbind unbind = new Unbind();
        defaultValueLoader.loadDefaultValues(unbind);
        BroadcastAck broadcastAck = sendUncheck(unbind);
        String status = broadcastAck.getAckMessageType().getStatus().getVal();

        isBound = false;
        if (status.equalsIgnoreCase(XMLConstant.Completed)) {
            logger.debug("Unbind successfully");
        } else {
            String failureReason = broadcastAck.getAckMessageType().getFailureReason();
            logger.debug("CBC return an error message: " + failureReason);
        }

        return broadcastAck;
    }

    public synchronized void shutdownConnection() throws UnsupportedEncodingException, ClientProtocolException, XmlApiException, IOException {
        if (httpClient != null) {
            logger.warn("Shuting down all connections to CBC");
//            unbind();
            httpClient.getConnectionManager().closeExpiredConnections();
            httpClient.getConnectionManager().closeIdleConnections(1, TimeUnit.MILLISECONDS);
            httpClient.getConnectionManager().shutdown();
            logger.warn("All connections to CBC were shutdown");
        }
    }

    private Request setTimeForBroadcastSubmitRequest(BroadcastSubmitRequest broadcastSubmitRequest) {
        Configuration config = ConfigurationManager.getConfiguration(Constant.SYS_CONFIG_NAME);
        long broadcastDelay = config.getLongProperty("broadcastDelayMs", 60000L, "general");
        long broadcastDuration = config.getLongProperty("broadcastDurationMs", 60000L, "general");

        Date broadcastStartTime = new Date(System.currentTimeMillis() + broadcastDelay);
        Date broadcastStopTime = new Date(System.currentTimeMillis() + broadcastDelay + broadcastDuration);
        SimpleDateFormat dateFormat = new SimpleDateFormat(XMLConstant.BroadcastDatePattern);

        broadcastSubmitRequest.setStartTime(dateFormat.format(broadcastStartTime));
        broadcastSubmitRequest.setStopTime(dateFormat.format(broadcastStopTime));

        return broadcastSubmitRequest;
    }

    private void loadBroadcastParams(BroadcastSubmitRequest request) {
        logger.debug("[LoadBroadcastParams] Start");
        Configuration configuration = ConfigurationManager.getConfiguration(Constant.SYS_CONFIG_NAME);
        String zoneName = request.getBroadcastAreaName();
        String numOfBroadcast = configuration.getProperty(zoneName + ".numOfBroadcast", "10", "broadcastParams");
        String repetitionPeriod = configuration.getProperty(zoneName + ".repetitionPeriod", "10", "broadcastParams");
        logger.debug("[LoadBroadcastParams] Zone: {} - numOfBroadcast {} - repetitionPeriod {}", new Object[]{zoneName, numOfBroadcast, repetitionPeriod});
        
        request.setRepetitionPeriod(repetitionPeriod);
        request.setNumOfBroadcast(numOfBroadcast);
        logger.debug("[LoadBroadcastParams] End");
    }
}
